import {useState, useRef, useCallback, useEffect, memo, type ReactElement, type ReactNode} from 'react';
import classNames from 'classnames';
import fastdom from 'fastdom';

import {FakeMoreButton, MoreButton} from './collapsible-more';
import getTabTitles from './collapsible-tab';
import {type TabProps} from './tab';

import styles from './tabs.css';

const DEFAULT_DEBOUNCE_INTERVAL = 100;
const MEASURE_TOLERANCE = 0.5;

export interface CollapsibleTabsProps {
  children: ReactElement<TabProps>[];
  selected?: string | undefined;
  onSelect?: ((key: string) => () => void) | undefined;
  onLastVisibleIndexChange?: ((index: number) => void) | null | undefined;
  moreClassName?: string | null | undefined;
  moreActiveClassName?: string | null | undefined;
  morePopupClassName?: string | null | undefined;
  morePopupItemClassName?: string | undefined;
  initialVisibleItems?: number | null | undefined;
  morePopupBeforeEnd?: ReactNode;
}
interface Sizes {
  tabs: number[];
  more: number | undefined;
}
interface PreparedElements {
  visible: ReactElement<TabProps>[];
  hidden: ReactElement<TabProps>[];
  ready?: boolean;
}
export const CollapsibleTabs = ({
  children,
  selected,
  onSelect,
  onLastVisibleIndexChange,
  moreClassName,
  moreActiveClassName,
  morePopupClassName,
  morePopupBeforeEnd,
  morePopupItemClassName,
  initialVisibleItems,
}: CollapsibleTabsProps) => {
  const [sizes, setSizes] = useState<Sizes>({tabs: [], more: undefined});
  const [lastVisibleIndex, setLastVisibleIndex] = useState<number | null>(null);
  const elements = {sizes, lastVisibleIndex};
  const [preparedElements, setPreparedElements] = useState<PreparedElements>({
    visible: [],
    hidden: [],
  });

  const measureRef = useRef<HTMLDivElement>(null);

  const selectedIndex =
    children.filter(tab => tab.props.alwaysHidden !== true).findIndex(tab => tab.props.id === selected) ?? null;

  let items;

  if (preparedElements.ready) {
    items = preparedElements.visible;
  } else {
    items = initialVisibleItems
      ? children.filter(item => item.props.alwaysHidden !== true).slice(0, initialVisibleItems)
      : [];
  }

  const visibleElements = getTabTitles({
    items,
    selected,
    onSelect,
  });

  const hiddenElements = (() => {
    if (preparedElements.ready) {
      return preparedElements.hidden;
    }
    if (initialVisibleItems) {
      return children.filter(item => !visibleElements.some(visibleItem => visibleItem.props.child === item));
    }
    return [];
  })();

  const adjustTabs = useCallback(
    (entry: ResizeObserverEntry) => {
      const containerWidth = entry.contentRect.width;

      const {tabs: tabsSizes, more = 0} = elements.sizes;

      let renderMore = children.some(tab => tab.props.alwaysHidden);

      const tabsToRender: number[] = [];
      let filledWidth = renderMore ? (more ?? 0) : 0;

      for (let i = 0; i < tabsSizes.length; i++) {
        if (filledWidth + tabsSizes[i] < containerWidth + MEASURE_TOLERANCE) {
          filledWidth += tabsSizes[i];
          tabsToRender.push(tabsSizes[i]);
        } else {
          break;
        }
      }

      if (tabsToRender.length < tabsSizes.length && !renderMore) {
        for (let i = tabsToRender.length - 1; i >= 0; i--) {
          if (filledWidth + more < containerWidth + MEASURE_TOLERANCE) {
            filledWidth += more;
            renderMore = true;
            break;
          } else {
            filledWidth -= tabsToRender[i];
            tabsToRender.pop();
          }
        }
      }

      if (selectedIndex > tabsToRender.length - 1) {
        const selectedWidth = tabsSizes[selectedIndex];
        for (let i = tabsToRender.length - 1; i >= 0; i--) {
          if (filledWidth + selectedWidth < containerWidth + MEASURE_TOLERANCE) {
            filledWidth += selectedWidth;
            break;
          } else {
            filledWidth -= tabsToRender[i];
            tabsToRender.pop();
          }
        }
      }

      const newLastVisibleIndex = tabsToRender.length - 1;
      if (elements.lastVisibleIndex !== newLastVisibleIndex) {
        setLastVisibleIndex(newLastVisibleIndex);
        onLastVisibleIndexChange?.(newLastVisibleIndex);
      }
    },
    [children, elements.lastVisibleIndex, elements.sizes, onLastVisibleIndexChange, selectedIndex],
  );

  // Prepare list of visible and hidden elements
  useEffect(() => {
    const timeout = setTimeout(() => {
      const res = children.reduce(
        (accumulator: PreparedElements, tab) => {
          if (tab.props.alwaysHidden !== true && accumulator.visible.length - 1 < (elements.lastVisibleIndex ?? 0)) {
            accumulator.visible.push(tab);
          } else {
            accumulator.hidden.push(tab);
          }

          return accumulator;
        },
        {visible: [], hidden: [], ready: elements.lastVisibleIndex !== null},
      );

      if (selectedIndex > (elements.lastVisibleIndex ?? 0)) {
        const selectedItem = children.find(tab => !tab.props.alwaysHidden && tab.props.id === selected);
        if (selectedItem !== null && selectedItem !== undefined) {
          res.visible.push(selectedItem);
        }
      }

      const allVisibleTheSame =
        res.visible.length === preparedElements.visible.length &&
        res.visible.every((item, index) => item === preparedElements.visible[index]);
      const allHiddenTheSame =
        res.hidden.length === preparedElements.hidden.length &&
        res.hidden.every((item, index) => item === preparedElements.hidden[index]);

      if (!allVisibleTheSame || !allHiddenTheSame || preparedElements.ready !== res.ready) {
        fastdom.mutate(() => setPreparedElements(res));
      }
    }, DEFAULT_DEBOUNCE_INTERVAL);

    return () => {
      clearTimeout(timeout);
    };
  }, [children, elements.lastVisibleIndex, preparedElements, selected, selectedIndex]);

  // Get list of all possibly visible elements to render in a measure container
  const childItems = children.filter(tab => tab.props.alwaysHidden !== true);
  const childrenToMeasure = getTabTitles({items: childItems, tabIndex: -1});

  // Initial measure for tabs and more button sizes
  useEffect(() => {
    if (measureRef.current === null) {
      return undefined;
    }

    const measureTask = fastdom.measure(() => {
      const container = measureRef.current;
      const descendants = [...(container?.children ?? [])] as HTMLElement[];
      const moreButton = descendants.pop();

      let moreButtonWidth = moreButton?.offsetWidth ?? 0;
      const {marginLeft: moreButtonMarginLeft = '0', marginRight: moreButtonMarginRight = '0'} = moreButton
        ? getComputedStyle(moreButton)
        : {};
      moreButtonWidth += +moreButtonMarginLeft.replace('px', '') + +moreButtonMarginRight.replace('px', '');

      const tabsWidth = descendants.map(node => {
        const {marginLeft, marginRight} = getComputedStyle(node);
        const width = node.getBoundingClientRect().width;
        return width + +marginLeft.replace('px', '') + +marginRight.replace('px', '');
      });

      const newSummaryWidth = tabsWidth.reduce((acc, curr) => acc + curr, 0);
      const oldSummaryWidth = elements.sizes.tabs.reduce((acc, curr) => acc + curr, 0);

      if (elements.sizes.more !== moreButtonWidth || newSummaryWidth !== oldSummaryWidth) {
        fastdom.mutate(() => setSizes({more: moreButtonWidth, tabs: tabsWidth}));
      }
    });

    return () => {
      fastdom.clear(measureTask);
    };
  }, [children, elements.sizes.more, elements.sizes.tabs]);

  // Start observers to listen resizing and mutation
  useEffect(() => {
    if (measureRef.current === null) {
      return undefined;
    }

    let measureTask = () => {};
    const resizeObserver = new ResizeObserver(entries => {
      entries.forEach(entry => {
        fastdom.clear(measureTask);
        measureTask = fastdom.mutate(() => adjustTabs(entry));
      });
    });

    resizeObserver.observe(measureRef.current);

    return () => {
      fastdom.clear(measureTask);
      resizeObserver.disconnect();
    };
  }, [adjustTabs]);

  const isAdjusted = (elements.lastVisibleIndex !== null && preparedElements.ready) || initialVisibleItems;

  const className = classNames(styles.titles, styles.autoCollapse, isAdjusted && styles.adjusted);

  return (
    <div className={styles.autoCollapseContainer}>
      <div className={classNames(className, styles.rendered)}>
        {visibleElements}
        <MoreButton
          moreClassName={moreClassName}
          moreActiveClassName={moreActiveClassName}
          morePopupClassName={morePopupClassName}
          morePopupBeforeEnd={morePopupBeforeEnd}
          morePopupItemClassName={morePopupItemClassName}
          items={hiddenElements}
          selected={selected}
          onSelect={onSelect}
        />
      </div>
      <div ref={measureRef} className={classNames(className, styles.measure)}>
        {childrenToMeasure}
        <FakeMoreButton
          hasActiveChildren={hiddenElements.some(item => item.props.alwaysHidden && item.props.id === selected)}
          moreClassName={moreClassName}
          moreActiveClassName={moreActiveClassName}
        />
      </div>
    </div>
  );
};

export default memo(CollapsibleTabs);
